WebMvcTest

@VERO
Created Date · 2023년 04월 24일 04:04
Last Updated Date · 2023년 04월 24일 04:04

WebMvcTest 어노테이션

WebMvcTest 는 Spring MVC controller를 테스트하기 위해 사용되는 Spring Boot의 어노테이션이다. 이 어노테이션은 전체 auto-configuration을 비활성화하고 대신 MVC 테스트와 관련된 구성만 적용한다. 즉, Application Context 를 완전하게 Start 하지 않고 Web Layer를 테스트 하고 싶을 때 @WebMvcTest 를 사용하는 것을 고려해볼 수 있다.

단위 테스트를 위해 Spring MVC 인프라를 자동으로 구성하지만, 스캔되는 Bean을 @Controller, @ControllerAdvice, @RestController, @JsonComponent, Filter, WebMvcConfigurer, HandlerMethodArgumentResolver 로 제한한다.
만약 Service나 Repository의 Dependency가 필요한 경우에는 @MockBean 으로 주입 받아 테스트를 진행한다.

@WebMvcTest를 사용할 때, 일반 @Component, @Service, @Repository bean은 스캔되지 않는다.
따라서 @MockBean 또는 @SpyBean 을 사용하여 가짜 객체를 bean으로 등록해주어야 한다.

@SpringBootTest

프로젝트 안의 모든 bean을 등록하여 테스트를 한다. 단위 테스트처럼 기능을 테스트할 때보다는 통합 테스트를 할 때 사용한다.

서버를 띄우고 모든 bean을 등록하기 때문에 다양한 테스트 중에서 가장 운영환경과 유사한 테스트이다.

그러나 모든 bean을 로드하기 때문에 테스트 구동 시간이 오래 걸리고, 테스트 단위가 크기 때문에 디버깅이 어려울 수 있다.

Controller 레이어만 테스트하고 싶을 때에는 @WebMvcTest 를 쓰는 것이 유용하다.

WebMvcTest 사용하기

기본적으로 @WebMvcTest 로 주석을 단 테스트는 Spring Security 및 MockMvc도 자동으로 구성한다.

테스트 클래스에 @WebMvcTest 어노테이션을 달고 테스트할 컨트롤러를 지정한다.

@WebMvcTest(RacingCarController.class)  
class RacingCarControllerTest

이렇게 테스트할 컨트롤러를 지정하게 되면 전체 애플리케이션 컨텍스트가 아닌 RacingCarController 만 구성하게 된다.

해당 테스트에 적용해서는 안 되는 auto-configuration 클래스를 제외하도록 excludeAutoConfiguration 옵션을 지정할 수도 있다.

@WebMvcTest(
    controllers = RacingCarController.class,
    excludeAutoConfiguration = SecurityAutoConfiguration.class,
    excludeFilters = {
        @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = SecurityConfig.class)}
)

excludeAutoConfiguration: 해당 테스트에 적용되는 자동 설정들에서 제외할 빈을 등록할 수 있다.
excludeFilters : 추가되는 bean들 중에 제외하고 싶은 Bean의 필터를 등록한다. 즉, classes에 들어갈 Bean을 제외하기 위한 필터를 정의하는 것이다. ASSIANABLE_TYPE 속성은 제외할 기준을 클래스로 지정한다.

@WebMvcTest(RacingCarController.class)   
class RacingCarControllerTest {  
  
    @Autowired  
    private MockMvc mockMvc;  // 주입 O  

	...
}

@WebMvcTest 만 선언해주어도, MockMvc 객체가 주입되게 된다.
또한 @MockBean 을 사용해서 필요한 의존성에 대해 mock 구현을 제공할 수도 있다.

Web Layer 관련 빈들만 등록하기 때문에, 컨트롤러는 주입이 정상적으로 되지만, @Component 로 등록된 Repository와 Service는 주입이 되지 않는다.

@WebMvcTest(RacingCarController.class)  
class RacingCarControllerTest {  
  
    @Autowired  
    private MockMvc mockMvc;  // 주입 가능하다.
  
    @Autowired  
    private RacingCarService racingCarService;  // 주입이 되지 않는다.  
	...
}

따라서, @WebMvcTest 에서 Repository와 Service를 사용하기 위해서는 @MockBean 을 사용하여 bean으로 등록해주어야 한다.

@MockBean 이란 가짜 객체로, 호출과 결과를 임의로 조작하여 해당 단위 테스트에만 집중할 수 있도록 도와준다.

@WebMvcTest(RacingCarController.class)  
class RacingCarControllerTest {  
  
    @Autowired  
    private MockMvc mockMvc;  
  
    @MockBean  
    private RacingCarService racingCarService;  // 주입이 가능하다.   
  
    @Mock  
    RandomMoveStrategy randomMoveStrategy;   // Service 내부에서 사용하는 RandomMoveStrategy Mocking   

	...
}

테스트 코드 작성해보기

    @Test  
    void play_메서드가_적절한_형식을_반환한다() throws Exception {  
        final PlayRequest playRequest = new PlayRequest("브리,토미,브라운", 1);  
        final String request = objectMapper.writeValueAsString(playRequest);  
        Car bri = new Car("브리");  
        Car tomi = new Car("토미");  
        Car brown = new Car("브라운");  
        List<Car> cars = List.of(bri);  
        final PlayResponse playResponse = PlayResponse.of(  
                RacingCarWinnerDto.of(cars),  
                List.of(  
                        RacingCarStatusDto.from(bri),  
                        RacingCarStatusDto.from(tomi),  
                        RacingCarStatusDto.from(brown)  
                )        );  
  
        // given  
        given(randomMoveStrategy.isMovable()).willReturn(true);  
        given(racingCarService.play(any(PlayRequest.class)))  
                .willReturn(playResponse);  
  
        // then  
        mockMvc.perform(post("/plays")  
                        .content(request)  
                        .contentType(MediaType.APPLICATION_JSON))  
                .andExpect(status().isOk())  
                .andExpect(jsonPath("$.winners[0]").value("브리"))  
                .andExpect(jsonPath("racingCars", hasSize(3)))  
                .andExpect(jsonPath("$.racingCars[0].name").value("브리"))  
                .andExpect(jsonPath("$.racingCars[0].position").value(0));  
    }
}

given(), when(), willReturn(), thenReturn() 메서드들은 BDDMockito에 정의되어 있다.

@MockBean 으로 등록한 Service의 행동을 given() 으로 지정해준다.
willReturn() 에서는 해당 행동에서 어떤 값을 리턴해야 하는지 지정한다.

perform() 에서 컨트롤러에 요청을 전송한다.
andExpect()에서는 응답을 검증하는 역할을 한다.
status() 은 상태 코드를 검증한다. content() 은 응답에 대한 정보를 검증할 수 있다.

WebMvcTest의 장단점

@WebMvcTest를 사용하면 Spring MVC에 집중한 테스트를 할 수 있다.
Web Layer에 필요한 bean들만 등록하기 때문에 상대적으로 빠르고 가벼운 테스트를 할 수 있다.
또한 통합 테스트가 어려운 상황에서 Mock으로 테스트할 수 있다는 장점이 있다.

그러나 요청부터 응답까지 모든 테스트를 Mock 기반으로 테스트하기 때문에 실제 환경에서는 오류가 발생할 수 있다는 단점이 있다.

이러한 이유로 @WebMvcTest 는 컨트롤러 테스트나 단위 테스트 시에 많이 사용한다.

추가 : @RunWith 은 안 써도 될까?

구글링을 하다보면 많은 코드에서 Test를 작성할 때 @RunWith(SpringRunner.class) 를 같이 써준 것을 볼 수 있다.

// 많이 찾아볼 수 있는 코드
@RunWith(SpringRunner.class) 
@WebMvcTest(RacingCarController.class) 
public class Test { 
	... 
}

JUnit4를 사용한다면 @RunWith(SpringRunner.class) 를 같이 추가해줘야지만 annotation이 무시되지 않는다.
JUnit5를 사용한다면 @RunWith을 추가해 줄 필요가 없다. JUnit5에서는 @WebMvcTest 에도 @RunWith 어노테이션이 내장되어 있다.

junit-dependency.png

이번 jwp-racingcar 미션에서는 JUnit5가 의존성에 포함되어 있기 때문에 테스트 코드마다 매번 @RunWith 를 써줄 필요가 없다.

참고 자료